﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Threading.Tasks;
using Wombat.Core.Diagnostics;
using Wombat.Core.Factories;
using Wombat.Core.Services;

namespace Wombat.Core.Gates
{
    /// <summary>
    /// A gate that can be loaded on demand (a new instance or out of a pool)
    /// to do some calculation on a tag.
    /// </summary>
    public abstract class CodeExecuterGate: IActionableGate, ITagToVariableMapper, IDiagnosticsObservable, IInitializable, IUniqueIdentifiableSetter, ITagValueObservable
    {
        #region Delegate Factory

        protected readonly IDynamicCodeServicePeer DynamicCodeServicePeer;

        #endregion

        #region Fields

        protected IDictionary<Guid, Guid> InputPortToTagMapping = new Dictionary<Guid, Guid>();

        private readonly ITagStateService _tagStateService;
        private readonly ITagValueFactory _tagValueFactory;
        private readonly IObservationManager _observationManager;
        private IObservablePort _resultPort;
        private List<IObserverPort<ITagValue>> _inputPorts = new List<IObserverPort<ITagValue>>();
        private readonly List<Tuple<IPort, Guid, string>> _tempPortToTagMappings = new List<Tuple<IPort, Guid, string>>();
        protected IDictionary<Guid, string> InputPortToVariableMapping = new Dictionary<Guid, string>();
        protected IDictionary<Guid, IPort> PortsById = new Dictionary<Guid, IPort>();

        #endregion

        #region Constructor

        protected CodeExecuterGate(IDynamicCodeServicePeer dynamicCodeServicePeer, ITagStateService tagStateService,
                                   ITagValueFactory tagValueFactory, IObservationManager observationManager)
        {
            DynamicCodeServicePeer = dynamicCodeServicePeer;
            _tagStateService = tagStateService;
            _tagValueFactory = tagValueFactory;
            _observationManager = observationManager;
        }

        #endregion

        #region Properties

        protected DateTime CurrentTime { get; set; }
        public Guid Guid { get; private set; }

        public IEnumerable<IPort> Ports
        {
            get { return _inputPorts.OfType<IPort>().Concat(new[] {_resultPort}); }
        }

        public string Code { get; set; }

        #endregion

        #region Private Methods

        private void InitializePorts(IList<Tuple<IPort, Guid, string>> mappings)
        {
            _inputPorts = mappings.Select(p => (IObserverPort<ITagValue>) p.Item1).ToList();
            InputPortToVariableMapping = mappings.ToDictionary(p => p.Item1.Guid, p => p.Item3);
            InputPortToTagMapping = mappings.ToDictionary(p => p.Item1.Guid, p => p.Item2);
            PortsById = _inputPorts.ToDictionary(p => p.Guid, p => (IPort) p);
        }

        private IObserverPort<ITagValue> CreateInputPort(Guid tagId)
        {
            var rv = (IObserverPort<ITagValue>)PortFactory.Create<ITagValue>("Input" + tagId, PortBehavior.Input, OnNextData);
            return rv;
        }

        private async void OnNextData(ITagValue tagValue)
        {
            if (tagValue.Time > CurrentTime)
                CurrentTime = tagValue.Time;
            await ExecuteAction(tagValue.Time);
        }

        #endregion

        #region ITagToVariableMapper Implementation

        /// <summary>
        /// Adds the mapping. One all the mappings are added, call the initialize method.
        /// </summary>
        /// <param name="tagId">The tag id.</param>
        /// <param name="variableName">Name of the variable.</param>
        Guid ITagToVariableMapper.AddMapping(Guid tagId, string variableName)
        {
            Contract.Assert(tagId != null && tagId != Guid.Empty);
            var newPort = (IPort) CreateInputPort(tagId);
            _tempPortToTagMappings.Add(Tuple.Create(newPort, tagId, variableName));
            return newPort.Guid;
        }

        #endregion


        #region IUniqueIdentifiableSetter Implementation

        void IUniqueIdentifiableSetter.SetGuid(Guid guid)
        {
            Guid = guid;
        }

        #endregion

        #region Protected and Public Methods

        /// <summary>
        /// Binds the output parameter values.
        /// </summary>
        protected virtual void BindOutputParameterValues(DateTime atTime, bool hasReturnValue, object returnValue)
        {
            if (hasReturnValue)
            {
                var newTagValue = _tagValueFactory.CreateNew(Guid, atTime, returnValue);

                _tagStateService.SetTagValue(newTagValue.TagId, atTime, newTagValue.ObjectValue);
                _observationManager.OnNext(this, newTagValue);
            }
            else
            {
                var newCurroptValue = _tagValueFactory.CreateCurropt(Guid, atTime);

                _tagStateService.CorruptTagValue(Guid, CurrentTime);
                _observationManager.OnNext(this, newCurroptValue);
            }
        }

        /// <summary>
        /// Binds the input parameter values.
        /// </summary>
        /// <returns>
        /// Weather the binding have been successful.
        /// </returns>
        protected virtual bool TryBindInputParameterValues(DateTime time, out IDictionary<string, object> parameters)
        {
            parameters = new Dictionary<string, object>();
            foreach (var portId in InputPortToVariableMapping.Keys)
            {
                var tagId = InputPortToTagMapping[portId];
                var tagValue = default(object);
                if (_tagStateService.TryGetTagValueAt(tagId, time, out tagValue))
                {
                    parameters.Add(InputPortToVariableMapping[portId], tagValue);
                }
                else
                    return false;
            }
            return true;
        }

        /// <summary>
        /// Executes the code.
        /// </summary>
        protected abstract object ExecuteCode(IDictionary<string, object> inputParameters);

        #endregion


        #region IActionableGate Implementation

        /// <summary>
        /// Executes the code. 
        /// Binds the input parameters to tag values, executes the code
        /// then binds the output paramters and publishes the output
        /// results on the output ports.
        /// Note: Object should be initialized.
        /// </summary>
        public async void Action()
        {
            await ExecuteAction(CurrentTime);
        }

        private async Task ExecuteAction(DateTime time)
        {
            await Task.Factory.StartNew
                (
                    () =>
                        {
                            var inParams = default(IDictionary<string, object>);
                            var hasReturnValue = default(bool);
                            var returnValue = default(object);
                            if (!TryBindInputParameterValues(time, out inParams)) return;
                            try
                            {
                                returnValue = ExecuteCode(inParams);
                                hasReturnValue = true;
                            }
                            catch (Exception exception)
                            {
                                hasReturnValue = false;
                                _observationManager.OnError(_resultPort, exception);
                            }
                            BindOutputParameterValues(time, hasReturnValue, returnValue);
                        }
                );
        }

        #endregion


        #region IDiagnosticsObservable Implementation

        public void SubscribeDiagnostics(DiagnosticsLevel level, IObserver<IDiagnosticsMessage> errorObserver)
        {
        }

        #endregion



        #region IInitializable Implementation

        /// <summary>
        /// Initializes this instance.
        /// Makes sure that the code is loaded and compiled
        /// and variable mappings are set.
        /// </summary>
        public virtual void Initialize()
        {
            _resultPort = (IObservablePort) PortFactory.Create("ResultPort", PortBehavior.Output, this);
            InitializePorts(_tempPortToTagMappings);
            _tempPortToTagMappings.Clear();
            DynamicCodeServicePeer.SetExpression(Code);
        }

        #endregion


        #region ITagValueObservable Implementation

        public IDisposable Subscribe<T>(string pattern, IObserver<T> observer) where T : ITagValue
        {
            return _observationManager.Subscribe(this, pattern, observer);
        }

        #endregion

    }
}
